//========================================================================
//
// This file is licensed under the GPLv2 or later
//
// Copyright (C) 2014 Rodrigo Rivas Costa <rodrigorivascosta@gmail.com>
// Copyright (C) 2014, 2017 Adrian Johnson <ajohnson@redneon.com>
// Copyright (C) 2017, 2018 Albert Astals Cid <aacid@kde.org>
//
// To see a description of the changes please see the Changelog file that
// came with your tarball or type make ChangeLog if you are building from git
//
//========================================================================

#include <cairo.h>
#ifdef CAIRO_HAS_WIN32_SURFACE

#include <cairo-win32.h>

#include "parseargs.h"
#include "pdftocairo-win32.h"
#include "Win32Console.h"

#include <dlgs.h>
#include <commctrl.h>
#include <commdlg.h>
#include <windowsx.h>
#include <winspool.h>

static HDC hdc;
static HGLOBAL hDevmode = nullptr;
static HGLOBAL hDevnames = nullptr;
static DEVMODEA *devmode;
static char *printerName;

struct Win32Option
{
  const char *name;
  int value;
};

static const Win32Option win32PaperSource[] =
{
  {"upper", DMBIN_UPPER},
  {"onlyone", DMBIN_ONLYONE},
  {"lower", DMBIN_LOWER},
  {"middle", DMBIN_MIDDLE},
  {"manual", DMBIN_MANUAL},
  {"envelope", DMBIN_ENVELOPE},
  {"envmanual", DMBIN_ENVMANUAL},
  {"auto", DMBIN_AUTO},
  {"tractor", DMBIN_TRACTOR},
  {"smallfmt", DMBIN_SMALLFMT},
  {"largefmt", DMBIN_LARGEFMT},
  {"largecapacity", DMBIN_LARGECAPACITY},
  {"formsource", DMBIN_FORMSOURCE},
  {nullptr, 0}
};

static void parseSource(GooString *source)
{
  const Win32Option *option = win32PaperSource;
  while (option->name) {
    if (source->cmp(option->name) == 0) {
      devmode->dmDefaultSource = option->value;
      devmode->dmFields |= DM_DEFAULTSOURCE;
      return;
    }
    option++;
  }
  fprintf(stderr, "Warning: Unknown paper source \"%s\"\n", source->c_str());
}

static const Win32Option win32DuplexMode[] =
{
  {"off", DMDUP_SIMPLEX},
  {"short", DMDUP_HORIZONTAL},
  {"long", DMDUP_VERTICAL},
  {nullptr, 0}
};

static void parseDuplex(GooString *mode)
{
  const Win32Option *option = win32DuplexMode;
  while (option->name) {
    if (mode->cmp(option->name) == 0) {
      devmode->dmDuplex = option->value;
      devmode->dmFields |= DM_DUPLEX;
      return;
    }
    option++;
  }
  fprintf(stderr, "Warning: Unknown duplex mode \"%s\"\n", mode->c_str());
}

static void fillCommonPrinterOptions(bool duplex)
{
  if (duplex) {
    devmode->dmDuplex = DMDUP_HORIZONTAL;
    devmode->dmFields |= DM_DUPLEX;
  }
}

static void fillPagePrinterOptions(double w, double h)
{
  w *= 254.0 / 72.0; // units are 0.1mm
  h *= 254.0 / 72.0;
  if (w > h) {
    devmode->dmOrientation = DMORIENT_LANDSCAPE;
    devmode->dmPaperWidth = h;
    devmode->dmPaperLength = w;
  } else {
    devmode->dmOrientation = DMORIENT_PORTRAIT;
    devmode->dmPaperWidth = w;
    devmode->dmPaperLength = h;
  }
  devmode->dmPaperSize = 0;
  devmode->dmFields |= DM_ORIENTATION | DM_PAPERWIDTH | DM_PAPERLENGTH;
}


static void fillPrinterOptions(bool duplex, GooString *printOpt)
{
  //printOpt format is: <opt1>=<val1>,<opt2>=<val2>,...
  const char *nextOpt = printOpt->c_str();
  while (nextOpt && *nextOpt)
  {
    const char *comma = strchr(nextOpt, ',');
    GooString opt;
    if (comma) {
      opt.Set(nextOpt, comma - nextOpt);
      nextOpt = comma + 1;
    } else {
      opt.Set(nextOpt);
      nextOpt = NULL;
    }
    //here opt is "<optN>=<valN> "
    const char *equal = strchr(opt.c_str(), '=');
    if (!equal) {
      fprintf(stderr, "Warning: unknown printer option \"%s\"\n", opt.c_str());
      continue;
    }
    int iequal = equal - opt.c_str();
    GooString value(&opt, iequal + 1, opt.getLength() - iequal - 1);
    opt.del(iequal, opt.getLength() - iequal);
    //here opt is "<optN>" and value is "<valN>"

    if (opt.cmp("source") == 0) {
      parseSource(&value);
    } else if (opt.cmp("duplex") == 0) {
      if (duplex)
	fprintf(stderr, "Warning: duplex mode is specified both as standalone and printer options\n");
      else
	parseDuplex( &value);
    } else {
      fprintf(stderr, "Warning: unknown printer option \"%s\"\n", opt.c_str());
    }
  }
}

static void getLocalPos(HWND wind, HWND dlg, RECT *rect)
{
  GetClientRect(wind, rect);
  MapWindowPoints(wind, dlg, (LPPOINT)rect, 2);
}

static HWND createGroupBox(HWND parent, HINSTANCE hInstance, HMENU id, const char *label, RECT *rect)
{
  HWND hwnd = CreateWindowA(WC_BUTTONA,
			    label,
			    WS_CHILD | WS_VISIBLE | BS_GROUPBOX,
			    rect->left, rect->top,
			    rect->right - rect->left,
			    rect->bottom - rect->top,
			    parent, id,
			    hInstance, NULL);
  HFONT hFont = (HFONT)SendMessage(parent, WM_GETFONT, (WPARAM)0, (LPARAM)0);
  if (hFont)
    SendMessage(hwnd, WM_SETFONT, (WPARAM) hFont, (LPARAM)0);
  return hwnd;
}

static HWND createCheckBox(HWND parent, HINSTANCE hInstance, HMENU id, const char *label, RECT *rect)
{
  HWND hwnd = CreateWindowA(WC_BUTTONA,
			    label,
			    WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX | WS_TABSTOP,
			    rect->left, rect->top,
			    rect->right - rect->left,
			    rect->bottom - rect->top,
			    parent, id,
			    hInstance, NULL);
  HFONT hFont = (HFONT)SendMessage(parent, WM_GETFONT, (WPARAM)0, (LPARAM)0);
  if (hFont)
    SendMessage(hwnd, WM_SETFONT, (WPARAM) hFont, (LPARAM)0);
  return hwnd;
}

static HWND createStaticText(HWND parent, HINSTANCE hinstance, HMENU id, const char *text, RECT *rect)
{
  HWND hwnd = CreateWindowA(WC_STATICA,
			    text,
			    WS_CHILD | WS_VISIBLE | SS_LEFT,
			    rect->left, rect->top,
			    rect->right - rect->left,
			    rect->bottom - rect->top,
			    parent, id,
			    hinstance, NULL);
  HFONT hFont = (HFONT)SendMessage(parent, WM_GETFONT, (WPARAM)0, (LPARAM)0);
  if (hFont)
    SendMessage(hwnd, WM_SETFONT, (WPARAM) hFont, (LPARAM)0);
  return hwnd;
}

static HWND createPageScaleComboBox(HWND parent, HINSTANCE hinstance, HMENU id, RECT *rect)
{
  HWND hwnd = CreateWindowA(WC_COMBOBOX,
			    "",
			    WS_CHILD | WS_VISIBLE | WS_GROUP | WS_TABSTOP |
			    CBS_DROPDOWNLIST,
			    rect->left, rect->top,
			    rect->right - rect->left,
			    rect->bottom - rect->top,
			    parent, id,
			    hinstance, NULL);
  HFONT hFont = (HFONT)SendMessage(parent, WM_GETFONT, (WPARAM)0, (LPARAM)0);
  if (hFont)
    SendMessage(hwnd, WM_SETFONT, (WPARAM) hFont, (LPARAM)0);
  ComboBox_AddString(hwnd, "None");
  ComboBox_AddString(hwnd, "Shrink to Printable Area");
  ComboBox_AddString(hwnd, "Fit to Printable Area");
  return hwnd;
}

enum PageScale { NONE = 0, SHRINK = 1, FIT = 2 };

// used to set/get option values in printDialogHookProc
static PageScale pageScale;
static bool centerPage;
static bool useOrigPageSize;

// PrintDlg callback to customize the print dialog with additional controls.
static UINT_PTR CALLBACK printDialogHookProc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
  if (uiMsg == WM_INITDIALOG) {
    // Get the extant controls. See dlgs.h and prnsetup.dlg for the PrintDlg control ids.
    HWND printerGroupWind = GetDlgItem(hdlg, grp4);
    HWND printerComboWind = GetDlgItem(hdlg, cmb4);
    HWND nameLabelWind = GetDlgItem(hdlg, stc6);
    HWND statusLabelWind = GetDlgItem(hdlg, stc8);
    HWND printRangeGroupWind = GetDlgItem(hdlg, grp1);
    HWND radio1Wind = GetDlgItem(hdlg, rad1);
    HWND radio2Wind = GetDlgItem(hdlg, rad3);
    HWND copiesGroupWind = GetDlgItem(hdlg, grp2);
    HWND okWind = GetDlgItem(hdlg, IDOK);
    HWND cancelWind = GetDlgItem(hdlg, IDCANCEL);
    if (!printerGroupWind || !printerComboWind || !nameLabelWind || !statusLabelWind ||
	!printRangeGroupWind || !radio1Wind || !radio2Wind || !copiesGroupWind ||
	!okWind || !cancelWind)
      return 0;

    // Get the size and position of the above controls relative to the
    // print dialog window
    RECT printerGroupRect;
    RECT printerComboRect;
    RECT nameLabelRect;
    RECT statusLabelRect;
    RECT printRangeGroupRect;
    RECT radio1Rect, radio2Rect;
    RECT copiesGroupRect;
    RECT okRect, cancelRect;
    getLocalPos(printerGroupWind, hdlg, &printerGroupRect);
    getLocalPos(printerComboWind, hdlg, &printerComboRect);
    getLocalPos(nameLabelWind, hdlg, &nameLabelRect);
    getLocalPos(statusLabelWind, hdlg, &statusLabelRect);
    getLocalPos(printRangeGroupWind, hdlg, &printRangeGroupRect);
    getLocalPos(radio1Wind, hdlg, &radio1Rect);
    getLocalPos(radio2Wind, hdlg, &radio2Rect);
    getLocalPos(copiesGroupWind, hdlg, &copiesGroupRect);
    getLocalPos(okWind, hdlg, &okRect);
    getLocalPos(cancelWind, hdlg, &cancelRect);

    // Calc space required for new group
    int interGroupSpace = printRangeGroupRect.top - printerGroupRect.bottom;
    int groupHeight = statusLabelRect.top - printerGroupRect.top +
      printRangeGroupRect.bottom - radio1Rect.bottom;

    // Increase dialog size
    RECT dlgRect;
    GetWindowRect(hdlg, &dlgRect);
    SetWindowPos(hdlg, nullptr,
		 dlgRect.left, dlgRect.top,
		 dlgRect.right - dlgRect.left,
		 dlgRect.bottom - dlgRect.top + interGroupSpace + groupHeight,
		 SWP_NOMOVE|SWP_NOREDRAW|SWP_NOZORDER);

    // Add new group and controls
    HINSTANCE hinstance = (HINSTANCE)GetWindowLongPtr(hdlg, GWLP_HINSTANCE);
    RECT pdfGroupBoxRect;
    pdfGroupBoxRect.left = printRangeGroupRect.left;
    pdfGroupBoxRect.right = copiesGroupRect.right;
    pdfGroupBoxRect.top = printRangeGroupRect.bottom + interGroupSpace;
    pdfGroupBoxRect.bottom = pdfGroupBoxRect.top + groupHeight;
    createGroupBox(hdlg, hinstance, (HMENU)grp3, "PDF Print Options", &pdfGroupBoxRect);

    RECT textRect;
    textRect.left = nameLabelRect.left;
    textRect.right = nameLabelRect.left + 1.8*(printerComboRect.left - nameLabelRect.left);
    textRect.top = pdfGroupBoxRect.top + nameLabelRect.top - printerGroupRect.top;
    textRect.bottom = textRect.top + nameLabelRect.bottom - nameLabelRect.top;
    createStaticText(hdlg, hinstance, (HMENU)stc1, "Page Scaling:", &textRect);

    RECT comboBoxRect;
    comboBoxRect.left = textRect.right;
    comboBoxRect.right = comboBoxRect.left + printerComboRect.right - printerComboRect.left;;
    comboBoxRect.top = pdfGroupBoxRect.top + printerComboRect.top - printerGroupRect.top;
    comboBoxRect.bottom = textRect.top + 4*(printerComboRect.bottom - printerComboRect.top);
    HWND comboBoxWind = createPageScaleComboBox(hdlg, hinstance, (HMENU)cmb1, &comboBoxRect);

    RECT checkBox1Rect;
    checkBox1Rect.left = radio1Rect.left;
    checkBox1Rect.right = pdfGroupBoxRect.right - 10;
    checkBox1Rect.top = pdfGroupBoxRect.top + statusLabelRect.top - printerGroupRect.top;
    checkBox1Rect.bottom = checkBox1Rect.top + radio1Rect.bottom - radio1Rect.top;
    HWND checkBox1Wind = createCheckBox(hdlg, hinstance, (HMENU)chx3, "Center", &checkBox1Rect);

    RECT checkBox2Rect;
    checkBox2Rect.left = radio1Rect.left;
    checkBox2Rect.right = pdfGroupBoxRect.right - 10;
    checkBox2Rect.top =  checkBox1Rect.top + radio2Rect.top - radio1Rect.top;
    checkBox2Rect.bottom = checkBox2Rect.top + radio1Rect.bottom - radio1Rect.top;
    HWND checkBox2Wind = createCheckBox(hdlg, hinstance, (HMENU)chx4, "Select page size using document page size", &checkBox2Rect);

    // Move OK and Cancel buttons down ensuring they are last in the Z order
    // so that the tab order is correct.
    SetWindowPos(okWind,
		 HWND_BOTTOM,
		 okRect.left,
		 okRect.top + interGroupSpace + groupHeight,
		 0, 0,
		 SWP_NOSIZE); // keep current size
    SetWindowPos(cancelWind,
		 HWND_BOTTOM,
		 cancelRect.left,
		 cancelRect.top + interGroupSpace + groupHeight,
		 0, 0,
		 SWP_NOSIZE); // keep current size

    // Initialize control values
    ComboBox_SetCurSel(comboBoxWind, pageScale);
    Button_SetCheck(checkBox1Wind, centerPage ? BST_CHECKED : BST_UNCHECKED);
    Button_SetCheck(checkBox2Wind, useOrigPageSize ? BST_CHECKED : BST_UNCHECKED);

  } else if (uiMsg == WM_COMMAND) {
    // Save settings
    UINT id = LOWORD(wParam);
    if (id == cmb1)
      pageScale = (PageScale)ComboBox_GetCurSel(GetDlgItem(hdlg, cmb1));
    if (id == chx3)
      centerPage = IsDlgButtonChecked(hdlg, chx3);
    if (id == chx4)
      useOrigPageSize = IsDlgButtonChecked(hdlg, chx4);
  }
  return 0;
}

void win32SetupPrinter(GooString *printer, GooString *printOpt,
		       bool duplex, bool setupdlg)
{
  if (printer->c_str()[0] == 0) {
    DWORD size = 0;
    GetDefaultPrinterA(nullptr, &size);
    printerName = (char*)gmalloc(size);
    GetDefaultPrinterA(printerName, &size);
  } else {
    printerName = copyString(printer->c_str(), printer->getLength());
  }

  //Query the size of the DEVMODE struct
  LONG szProp = DocumentPropertiesA(nullptr, nullptr, printerName, nullptr, nullptr, 0);
  if (szProp < 0) {
    fprintf(stderr, "Error: Printer \"%s\" not found\n", printerName);
    exit(99);
  }
  devmode = (DEVMODEA*)gmalloc(szProp);
  memset(devmode, 0, szProp);
  devmode->dmSize = sizeof(DEVMODEA);
  devmode->dmSpecVersion = DM_SPECVERSION;
  //Load the current default configuration for the printer into devmode
  if (DocumentPropertiesA(nullptr, nullptr, printerName, devmode, devmode, DM_OUT_BUFFER) < 0) {
    fprintf(stderr, "Error: Printer \"%s\" not found\n", printerName);
    exit(99);
  }

  // Update devmode with selected print options
  fillCommonPrinterOptions(duplex);
  fillPrinterOptions(duplex, printOpt);

  // Call DocumentProperties again so the driver can update its private data
  // with the modified print options. This will also display the printer
  // properties dialog if setupdlg is true.
  int ret;
  DWORD mode = DM_IN_BUFFER | DM_OUT_BUFFER;
  if (setupdlg)
    mode |= DM_IN_PROMPT;
  ret = DocumentPropertiesA(nullptr, nullptr, printerName, devmode, devmode, mode);
  if (ret < 0) {
    fprintf(stderr, "Error: Printer \"%s\" not found\n", printerName);
    exit(99);
  }
  if (setupdlg && ret == IDCANCEL)
    exit(0);

  hdc = CreateDCA(nullptr, printerName, nullptr, devmode);
  if (!hdc) {
    fprintf(stderr, "Error: Printer \"%s\" not found\n", printerName);
    exit(99);
  }
}

void win32ShowPrintDialog(bool *expand, bool *noShrink, bool *noCenter,
			  bool *usePDFPageSize, bool *allPages,
			  int *firstPage, int *lastPage, int maxPages)
{
  PRINTDLG pd;
  memset(&pd, 0, sizeof(PRINTDLG));
  pd.lStructSize = sizeof(PRINTDLG);
  pd.Flags = PD_NOSELECTION | PD_ENABLEPRINTHOOK | PD_USEDEVMODECOPIESANDCOLLATE | PD_RETURNDC;
  if (*allPages) {
    pd.nFromPage = 1;
    pd.nToPage = maxPages;
  } else {
    pd.Flags |= PD_PAGENUMS;
    pd.nFromPage = *firstPage;
    pd.nToPage = *lastPage;
  }
  pd.nCopies = 1;
  pd.nMinPage = 1;
  pd.nMaxPage = maxPages;
  pd.lpfnPrintHook = printDialogHookProc;
  if (!*expand && *noShrink)
    pageScale = NONE;
  else if (!*expand && !*noShrink)
    pageScale = SHRINK;
  else
    pageScale = FIT;
  centerPage = !*noCenter;
  useOrigPageSize = *usePDFPageSize;

  if (PrintDlgA(&pd)) {
    // Ok
    hDevnames = pd.hDevNames;
    DEVNAMES *devnames = (DEVNAMES*)GlobalLock(hDevnames);
    printerName = (char*)devnames + devnames->wDeviceOffset;
    hDevmode = pd.hDevMode;
    devmode = (DEVMODEA*)GlobalLock(hDevmode);
    hdc = pd.hDC;
    if (pd.Flags & PD_PAGENUMS) {
      *allPages = false;
      *firstPage = pd.nFromPage;
      *lastPage = pd.nToPage;
    } else {
      *allPages = true;
    }
    if (pageScale == NONE) {
      *expand = false;
      *noShrink = true;
    } else if (pageScale == SHRINK) {
      *expand = false;
      *noShrink = false;
    } else {
      *expand = true;
      *noShrink = false;
    }
    *noCenter = !centerPage;
    *usePDFPageSize = useOrigPageSize;
  } else {
    // Cancel
    exit(0);
  }
}

cairo_surface_t *win32BeginDocument(GooString *inputFileName, GooString *outputFileName)
{
  DOCINFOA docinfo;
  memset(&docinfo, 0, sizeof(docinfo));
  docinfo.cbSize = sizeof(docinfo);
  if (inputFileName->cmp("fd://0") == 0)
    docinfo.lpszDocName = "pdftocairo <stdin>";
  else
    docinfo.lpszDocName = inputFileName->c_str();
  if (outputFileName)
    docinfo.lpszOutput = outputFileName->c_str();
  if (StartDocA(hdc, &docinfo) <=0) {
    fprintf(stderr, "Error: StartDoc failed\n");
    exit(99);
  }

  return cairo_win32_printing_surface_create(hdc);
}

void win32BeginPage(double *w, double *h, bool changePageSize, bool useFullPage)
{
  if (changePageSize)
    fillPagePrinterOptions(*w, *h);
  if (DocumentPropertiesA(nullptr, nullptr, printerName, devmode, devmode, DM_IN_BUFFER | DM_OUT_BUFFER) < 0) {
    fprintf(stderr, "Error: Printer \"%s\" not found\n", printerName);
    exit(99);
  }
  ResetDCA(hdc, devmode);

  // Get actual paper size or if useFullPage is false the printable area.
  // Transform the hdc scale to points to be consistent with other cairo backends
  int x_dpi = GetDeviceCaps (hdc, LOGPIXELSX);
  int y_dpi = GetDeviceCaps (hdc, LOGPIXELSY);
  int x_off = GetDeviceCaps (hdc, PHYSICALOFFSETX);
  int y_off = GetDeviceCaps (hdc, PHYSICALOFFSETY);
  if (useFullPage) {
    *w = GetDeviceCaps (hdc, PHYSICALWIDTH)*72.0/x_dpi;
    *h = GetDeviceCaps (hdc, PHYSICALHEIGHT)*72.0/y_dpi;
  } else {
    *w = GetDeviceCaps (hdc, HORZRES)*72.0/x_dpi;
    *h = GetDeviceCaps (hdc, VERTRES)*72.0/y_dpi;
  }
  XFORM xform;
  xform.eM11 = x_dpi/72.0;
  xform.eM12 = 0;
  xform.eM21 = 0;
  xform.eM22 = y_dpi/72.0;
  if (useFullPage) {
    xform.eDx = -x_off;
    xform.eDy = -y_off;
  } else {
    xform.eDx = 0;
    xform.eDy = 0;
  }
  SetGraphicsMode (hdc, GM_ADVANCED);
  SetWorldTransform (hdc, &xform);

  StartPage(hdc);
}

void win32EndPage(GooString *imageFileName)
{
  EndPage(hdc);
}

void win32EndDocument()
{
  EndDoc(hdc);
  DeleteDC(hdc);
  if (hDevmode) {
    GlobalUnlock(hDevmode);
    GlobalFree(hDevmode);
  } else {
    gfree(devmode);
  }
  if (hDevnames) {
    GlobalUnlock(hDevnames);
    GlobalFree(hDevnames);
  } else {
    gfree(printerName);
  }
}

#endif // CAIRO_HAS_WIN32_SURFACE
